﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Reflection;
using System.DirectoryServices;
using Novell.Directory.Ldap;
using System.Collections;
using System.Globalization;
namespace Novell.DirectoryServices.Linq
{
    /// <summary>
    /// Represents an updatable directory source.  You can also insert, update, and delete objects.
    /// </summary>
    /// <typeparam name="T">A Type that derives from DirectoryEntity that represents the directory object.</typeparam>
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1710",Justification="This object does not represent an in memory collection but instead it represents a virtual one.")]
    public sealed class UpdatableDirectorySource<T> : DirectorySource<T>, IUpdatableDirectorySource where T : DirectoryEntity
    {
        #region Constructors

        ///// <summary>
        ///// Creates a new data source instance for the given directory search root and with a given search scope.
        ///// </summary>
        ///// <param name="searchRoot">Root location in the directory to start all searches from.</param>
        ///// <param name="searchScope">Search scope for all queries performed through this data source.</param>
        //public UpdatableDirectorySource(DirectoryEntry searchRoot, SearchScope searchScope)
        //    :base(searchRoot,searchScope)
        //{}

        ///// <summary>
        ///// Creates a new data source instance based on the given DirectorySearcher.
        ///// </summary>
        ///// <param name="searcher">DirectorySearcher object to use for directory searches.</param>
        //public UpdatableDirectorySource(DirectorySearcher searcher):base(searcher)
        //{}

        /// <summary>
        /// Creates a data source from the given context.
        /// </summary>
        /// <param name="context">DirectoryContext that embeds the data source.</param>
        /// <param name="searchOptions">Search options for all queries performed through this data source.</param>
        internal UpdatableDirectorySource(DirectoryContext context, SearchOptions searchOptions)
            :base(context,searchOptions)
        {}

        /// <summary>
        /// Creates a data source from the given context.
        /// </summary>
        /// <param name="context">The parent DirectoryEntity for this object.</param>
        /// <param name="searchScope">SearchScope for all queries performed through this data source.</param>
        internal UpdatableDirectorySource(DirectoryEntity entity, SearchScope searchScope)
            : base(entity, searchScope)
        {}
        #endregion

        /// <summary>
        /// Update catalog; keeps track of update entity instances.
        /// </summary>
        private Dictionary<T, HashSet<string>> updates = new Dictionary<T, HashSet<string>>();
        private List<T> deletes = new List<T>();
        private List<T> inserts = new List<T>();
        private List<T> objectsToTrack = new List<T>();

        public void InsertOnSubmit(T item)
        {
            if (item.IsNew == false)
            {
                //TODO: Support moving the object.
                throw new InvalidOperationException("Unable to insert the object because it is marked as already existing.");
            }
            //DirectoryEntity entity = item;
            //if (entity != null)
            //    entity.DirectoryEntry = e;

            inserts.Add(item);
            ////Add all of the properties of the item
            ////foreach property that has [DirectoryAttribute] Added to it.
            ////updates[item].Add(e.PropertyName);
            //foreach (PropertyInfo propInfo in item.GetType().GetProperties())
            //{
            //    DirectoryAttributeAttribute[] da = propInfo.GetCustomAttributes(typeof(DirectoryAttributeAttribute), false) as DirectoryAttributeAttribute[];
            //    if (da != null && da.Length != 0 && da[0] != null)
            //    {
            //        //Add the property
            //        updates[item].Add(propInfo.Name);
            //    }
            //}
            //TODO: Define default attribute (for dn creation)
            //_searcher.SearchRoot.Path + "
            //TODO: Need to Set the DN from this context.
            //TODO:Do we have to do anything to the item before it is inserted?
            //What about deletes?
        }
        public void DeleteOnSubmit(T item)
        {
            if (item.IsNew)
            {
                inserts.Remove(item);
            }
            else
            {
                updates.Remove(item);
                objectsToTrack.Remove(item);
            }
            deletes.Add(item);
        }
        void IUpdatableDirectorySource.SubmitChanges()
        {
            ProcessDeletesInternal();
            ProcessUpdatesInternal();
            ProcessInsertsInternal();

            foreach (DirectoryEntity entity in objectsToTrack)
            {
                entity.SubmitChangesInternal();
            }
        }
        void IUpdatableDirectorySource.UpdateNotification(object sender, PropertyChangedEventArgs e)
        {
            T source = (T)sender;

            if (!updates.ContainsKey(source))
                updates.Add(source, new HashSet<string>());

            updates[source].Add(e.PropertyName);
        }
        void IUpdatableDirectorySource.RegisterForSubmissionNotifications(DirectoryEntity entity)
        {
            objectsToTrack.Add((T)entity);
        }


        private void ProcessInsertsInternal()
        {
            Type t = typeof(T);
            PropertyInfo[] properties = t.GetProperties();
            //string defaultNamingAttribute = null;
            //We want to inherit so that we can discover all properties in the heirarchy
            //DirectorySchemaAttribute[] schemaAttributes = (DirectorySchemaAttribute[])t.GetCustomAttributes(typeof(DirectorySchemaAttribute), true);

            foreach (T item in inserts)
            {
                //Determine the state of the associated DirectoryEntry.
                //For new objects this should probably be null, but the source shouldn't be null.
                
                LdapEntry entry = item.DirectoryEntry;
                //Should the entry be not null for inserts?  I don't think so...
                if (entry != null)
                {
                    throw new InvalidOperationException("DirectoryEntry should not be set for the item");
                }

                if (string.IsNullOrEmpty(item.DefaultNamingAttribute))
                {
                    throw new InvalidOperationException("Can not insert the entity " + typeof(T).ToString() + " because it does not have a defaultNamingAttribute set");
                }
                string dn = string.Format(CultureInfo.InvariantCulture,"{0}={1},{2}"
                    , item.DefaultNamingAttribute
                    , item.DefaultNamingAttributeValue
                    , (this as IUpdatableDirectorySource).Root);
                LdapAttributeSet attributes = GetDirectoryEntryForNewItem(item);

                //DirectorySchemaAttribute[] schemaTypes = item.
                //Determine if the entery is new... Maybe this should be a different collection.
                //or another property added
                
                //Set all of the properties
                SetPropertiesFromItem(properties, item, attributes);

                //TODO: Must handle the case where the parent container doesn't exist.
                
                //Don't need to set the name, as it should be set automatically once the entry is committed.
                //entry.Name = namingAttributeValue;
                //This only seems to be needed 
                entry = new LdapEntry(dn, attributes);

                (this as IUpdatableDirectorySource).Context.Connection.Add(entry);

                item.InitializeInternal(this, entry);
                
            }
            inserts.Clear();
            //Ensure the DirectoryEntry is right
            //Set the right path.
            //Set the properties.  (Use update code?)
        }
        private void ProcessUpdatesInternal()
        {
            Type t = typeof(T);

            foreach (var e in updates)
            {
                if (e.Key is T && e.Key is DirectoryEntity)
                {
                    LdapEntry entry = ((DirectoryEntity)e.Key).DirectoryEntry;

                    //Determine if the entery is new... Maybe this should be a different collection.
                    //or another property added
                    List<LdapModification> updatesModifications = new List<LdapModification>();

                    foreach (string property in e.Value)
                    {
                        PropertyInfo i = t.GetProperty(property);
                        DirectoryAttributeAttribute[] da = i.GetCustomAttributes(typeof(DirectoryAttributeAttribute), false) as DirectoryAttributeAttribute[];
                        if (da != null && da.Length != 0 && da[0] != null)
                        {
                            
                            //Determine if the property has changed
                            LdapAttribute existingAttribute = entry.getAttribute(da[0].Attribute);
                            object newVal = i.GetValue(e.Key, null);

                            if (existingAttribute == null 
                                && !((newVal == null || string.IsNullOrEmpty(newVal.ToString()))))
                            {
                                //Insert an attribute
                                LdapAttribute att = GetLdapAttributeFromItemProperty((T)e.Key,i);
                                updatesModifications.Add(new LdapModification(LdapModification.ADD, att));
                            }
                            else if (existingAttribute != null && newVal == null || string.IsNullOrEmpty(newVal.ToString()))
                            {
                                //Delete the attribute
                                updatesModifications.Add(new LdapModification(LdapModification.DELETE, existingAttribute));
                            }
                            else if(existingAttribute != null)
                            {
                                updatesModifications.Add(new LdapModification(LdapModification.REPLACE
                                    , GetLdapAttributeFromItemProperty((T)e.Key, i)));
                                //Update an existing attribute.
                            }
                        }
                    }
                    (this as IUpdatableDirectorySource).Context.Connection.Modify(entry.DN, updatesModifications.ToArray());
                }
                else
                    throw new InvalidOperationException("Can't apply update because updates type doesn't match original entity type.");
            }

            updates.Clear();
        }
        private void ProcessDeletesInternal()
        {
            foreach (T item in deletes)
            {
                //If the item was inserted then deleted tehn remove it from the insertion queue since it is last.
                if (inserts.Contains(item))
                {
                    inserts.Remove(item);
                }
                else
                {
                    //itterate through the child items that are updatable. 
                    DeleteRecursive(item.DirectoryEntry);

                    //Delete the entry from the directory
                    //item.DirectoryEntry.DeleteTree();
                    //Commit the changes (Is this even needed?
                    //item.DirectoryEntry.CommitChanges();
                    //Reinitialize the object to not have a directory entry
                    //This marks it as new.
                    item.InitializeInternal(this, null);
                }
            }
            deletes.Clear();
        }
        private static LdapAttributeSet GetDirectoryEntryForNewItem(T item)
        {
            LdapAttributeSet newAttributes = new LdapAttributeSet();
            newAttributes.Add(new LdapAttribute("objectClass", item.ObjectClasses));
            if (item.DefaultNamingAttributeValue == null || string.IsNullOrEmpty(item.DefaultNamingAttributeValue))
            {
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,"The property {0} must have a value.", item.DefaultNamingAttribute));
            }
            //This will happen later when SetPropertiesFromItem is called.
            //newAttributes.Add(new LdapAttribute(item.DefaultNamingAttribute, item.DefaultNamingAttributeValue));
            return newAttributes;
        }
        
        private static void SetPropertiesFromItem(PropertyInfo[] properties, T item, LdapAttributeSet attributes)
        {
            //Foreach property, if it has an attribute defined, set the property on the entry.
            foreach (PropertyInfo prop in properties)
            {
                LdapAttribute att = GetLdapAttributeFromItemProperty(item, prop);
                if (att != null)
                {
                    attributes.Add(att);
                }
            }
        }

        private static LdapAttribute GetLdapAttributeFromItemProperty(T item, PropertyInfo prop)
        {
            DirectoryAttributeAttribute[] da = prop.GetCustomAttributes(typeof(DirectoryAttributeAttribute), false) as DirectoryAttributeAttribute[];
            if (da != null && da.Length != 0 && da[0] != null)//There will only ever be one attribute per property
            {
                //LdapAttribute a = attributes.getAttribute(da[0].Attribute);
                //if (a == null)

                object o = prop.GetValue(item, null);
                if (o != null)
                {
                    LdapAttribute a = new LdapAttribute(da[0].Attribute);
                    Type oType = o.GetType();
                    //Here we deal with types.

                    if (oType.IsAssignableFrom(typeof(byte[])))
                    {
                        AssignBase64Value(a, o);
                    }
                    else if (oType.IsAssignableFrom(typeof(IEnumerable)) || oType.IsArray)
                    {
                        AssignIEnumerableValue(a, o);
                    }
                    else if (o != null)
                    {
                        AssignSingleValue(a, o);
                    }
                    return a;
                }
            }
            return null;
            
        }
        private static void AssignSingleValue(LdapAttribute a, object o)
        {
            //For single value items, we want to remove any existing values just in case.
            if(a.StringValue != null)
                a.removeValue(a.StringValue); 
            a.addValue(o.ToString());
        }
        private static void AssignIEnumerableValue(LdapAttribute a, object o)
        {
            string[] values = a.StringValueArray;
            foreach (string v in values)
            {
                a.removeValue(v);
            }
            foreach (object ob in (o as IEnumerable))
            {
                a.addValue(ob.ToString());
            }
        }
        private static void AssignBase64Value(LdapAttribute a, object o)
        {
            byte[] bytes = (byte[])o;
            string base64 = Convert.ToBase64String(bytes);
            //Byte[] or String values???
            a.removeValue(a.StringValue); 
            a.addBase64Value(base64);
        }
#if ENABLE_REQUIRED_ATTRIBUTES
        private void SetPropertiesForRequiredAttributes(T item, string schema, LdapAttributeSet attributes)
        {
            if (item.RequiredAttributes.ContainsKey(schema))
            {
                List<string> requiredProperties = item.RequiredAttributes[schema];
                PropertyInfo[] allProperties = typeof(T).GetProperties();
                var v = from p in allProperties
                        from a in p.GetCustomAttributes(typeof(DirectoryAttributeAttribute), true)
                        where requiredProperties.Contains(((DirectoryAttributeAttribute)a).Attribute)
                        select p;

                //TODO: Need to throw an error if a property is not set for a particular attribute... Hmm...
                SetPropertiesFromItem(v.ToArray<PropertyInfo>(), item, attributes);
            }
        }
#endif
        /// <summary>
        /// Deletes an entry and its children
        /// </summary>
        /// <param name="directoryEntry">The entry to delete.</param>
        private void DeleteRecursive(LdapEntry directoryEntry)
        {
            LdapConnection conn = (this as IUpdatableDirectorySource).Context.Connection;

            LdapSearchResults results = conn.Search(directoryEntry.DN, LdapConnection.SCOPE_ONE, "(objectClass=*)", new string[] { "objectClass", "dn" }, false);
            while (results.hasMore())
            {
                DeleteRecursive(results.next());
            }
            conn.Delete(directoryEntry.DN);
        }
    }
}
